package org.atlantis.grosbeak.json;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;

import org.atlantis.grosbeak.lang.Exceptions;
import org.atlantis.grosbeak.lang.FailToMirrorException;
import org.atlantis.grosbeak.lang.Mirror;
import org.atlantis.grosbeak.lang.Strings;

class JsonRender {

	private static String NL = "\n";
	private static final Pattern PTN = Pattern
			.compile("^[a-z_A-Z$]+[a-zA-Z_0-9$]*$");
	private HashMap<Object, Object> memo;
	private Writer writer;
	private JsonHandler handler;

	JsonRender(Writer writer, JsonHandler handler) {
		this.writer = writer;
		this.handler = handler;
		memo = new HashMap<Object, Object>();
	}

	private static boolean isCompress(JsonRender render) {
		return render.handler.isCompress();
	}

	private void appendName(String name) throws IOException {
		if (handler.isQuoteName() || !PTN.matcher(name).find())
			stringToJson(name);
		else
			writer.append(name);
	}

	private void appendPairBegin() throws IOException {
		if (!isCompress(this))
			writer.append(NL).append(
					Strings.duplicate(handler.getIndentBy(), handler
							.getIndent()));
	}

	private void appendPairSep() throws IOException {
		writer.append(!isCompress(this) ? " :" : ":");
	}

	private void appendPair(String name, Object value) throws IOException {
		appendPairBegin();
		appendName(name);
		appendPairSep();
		rendering(value);
	}

	private boolean isIgnore(String key, Object value) {
		if (null == value && handler.isIgnoreNull())
			return true;
		return handler.ignore(key);
	}

	private void appendPairEnd() throws IOException {
		writer.append(',');
	}

	private void appendBraceBegin() throws IOException {
		writer.append("{");
	}

	private void appendBraceEnd() throws IOException {
		if (!isCompress(this))
			writer.append(NL).append(
					Strings.duplicate(handler.getIndentBy(), handler
							.getIndent()));
		writer.append("}");
	}

	static class Pair {
		String key;
		Object value;

		public Pair(String key, Object value) {
			this.key = key;
			this.value = value;
		}
	}

	@SuppressWarnings("unchecked")
	private void mapToJson(Map map) throws IOException {
		if (null == map)
			return;
		appendBraceBegin();
		increaseHandlerIndent();
		ArrayList<Pair> list = new ArrayList<Pair>();
		for (Object _key : map.keySet()) {
			String key = (null == _key) ? "null" : _key.toString();
			Object value = map.get(key);
			if (!isIgnore(key, value))
				list.add(new Pair(key, value));
		}
		for (Iterator<Pair> iter = list.iterator(); iter.hasNext();) {
			Pair pair = iter.next();
			appendPair(pair.key, pair.value);
			if (iter.hasNext())
				appendPairEnd();
		}
		decreaseHandlerIndent();
		appendBraceEnd();
	}

	private void pojoToJson(Object obj) throws IOException {
		if (null == obj)
			return;
		Class<? extends Object> type = obj.getClass();
		Json json = type.getAnnotation(Json.class);
		if (null != json) {
			try {
				Method method = type.getMethod(json.value(), JsonHandler.class);
				Object re = method.invoke(obj, handler);
				writer.append(re.toString());
			} catch (Exception e) {
				throw Exceptions.makeThrow(JsonException.class, e, "");
			}
		} else {
			Mirror<?> me = Mirror.me(type);
			Field[] fields = me.getFields();
			appendBraceBegin();
			increaseHandlerIndent();
			ArrayList<Pair> list = new ArrayList<Pair>(fields.length);
			for (Field field : fields) {
				String name = field.getName();
				try {
					Object value = me.getValue(obj, field);
					if (!isIgnore(name, value))
						list.add(new Pair(name, value));
				} catch (FailToMirrorException e) {
				}
			}
			for (Iterator<Pair> iter = list.iterator(); iter.hasNext();) {
				Pair pair = iter.next();
				appendPair(pair.key, pair.value);
				if (iter.hasNext())
					appendPairEnd();
			}
			decreaseHandlerIndent();
			appendBraceEnd();
		}
	}

	private void stringToJson(String str) throws IOException {
		if (null == str) {
			writer.append("null");
		} else {
			char[] cs = str.toCharArray();
			writer.append('"');
			for (char c : cs) {
				switch (c) {
				case '"':
					writer.append("\\\"");
					break;
				case '\n':
					writer.append("\\n");
					break;
				case '\t':
					writer.append("\\t");
					break;
				case '\r':
					writer.append("\\r");
					break;
				case '\\':
					writer.append("\\\\");
					break;
				default:
					writer.append(c);
				}
			}
			writer.append('"');
		}
	}

	@SuppressWarnings("unchecked")
	void rendering(Object obj) throws IOException {
		if (null == obj) {
			writer.write("null");
		} else if (obj instanceof Class) {
			stringToJson(((Class<?>) obj).getName());
		} else if (obj instanceof Mirror) {
			stringToJson(((Mirror<?>) obj).getMirrorType().getName());
		} else {
			Mirror mr = Mirror.me(obj.getClass());
			if (mr.isEnum()) {
				stringToJson(((Enum) obj).name());
			} else if (mr.isNumber() || mr.isBoolean() || mr.isChar()) {
				writer.append(obj.toString());
			} else if (mr.as(CharSequence.class)) {
				stringToJson(obj.toString());
			} else if (mr.isDateTimeLike()) {
				stringToJson(handler.getCastors().castToString(obj));
			} else if (memo.containsKey(obj)) {
				writer.append("null");
			} else {
				if (obj instanceof Map)
					mapToJson((Map) obj);
				else if (obj instanceof Collection)
					collectionToJson((Collection) obj);
				else if (obj.getClass().isArray())
					arrayToJson(obj);
				else {
					memo.put(obj, null);
					pojoToJson(obj);
					memo.remove(obj);
				}
			}
		}

	}

	private void decreaseHandlerIndent() {
		if (!isCompress(this))
			handler.decreaseIndent();
	}

	private void increaseHandlerIndent() {
		if (!isCompress(this))
			handler.increaseIndent();
	}

	private void arrayToJson(Object obj) throws IOException {
		writer.append('[');
		int len = Array.getLength(obj) - 1;
		if (len > -1) {
			int i;
			for (i = 0; i < len; i++) {
				rendering(Array.get(obj, i));
				writer.append(',').append(' ');
			}
			rendering(Array.get(obj, i));
		}
		writer.append(']');
	}

	private void collectionToJson(Collection<?> obj) throws IOException {
		writer.append('[');
		for (Iterator<?> iter = obj.iterator(); iter.hasNext();) {
			rendering(iter.next());
			if (iter.hasNext())
				writer.append(',').append(' ');
		}
		writer.append(']');
	}
}
